{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Slots and Single Inheritance"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "First let's create a simple class hierarchy that does not use slots:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    def __init__(self, name):\n",
    "        self.name = name\n",
    "        \n",
    "class Student(Person):\n",
    "    pass"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If we create an instance of `Student`, we'll see that the `name` attribute is stored in the instance dictionary:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'name': 'Alex'}"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "s = Student('Alex')\n",
    "s.__dict__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's do the same thing, but use slots for the `Person` class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    __slots__ = 'name',\n",
    "    \n",
    "    def __init__(self, name):\n",
    "        self.name = name\n",
    "\n",
    "class Student(Person):\n",
    "    pass"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We know `Person` instances do not have a dictionary:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "'Person' object has no attribute '__dict__'\n"
     ]
    }
   ],
   "source": [
    "p = Person('Eric')\n",
    "try:\n",
    "    print(p.__dict__)\n",
    "except AttributeError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "But the sub class does:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = Student('Alex')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "('Alex', {})"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "s.name, s.__dict__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see, the `Student` instance `s` has a dictionary - but note that the dictionary does not contain the `name` property - that is still stored in a slot."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So, `name` uses a slot, but the `Student` instance has an instance dictionary, which means we can add instance attributes to it:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "s.age = 19"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'age': 19}"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "s.__dict__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "('Alex', 19)"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "s.name, s.age"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If we want our subclass to only use slots, we just need to specify a `__slots__` class attribute for it too:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Student(Person):\n",
    "    __slots__ = tuple()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'Alex'"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "s = Student('Alex')\n",
    "s.name"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And the `Student` instance no longer has an instance dictionary:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "'Student' object has no attribute '__dict__'\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    print(s.__dict__)\n",
    "except AttributeError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Of course, we did not add to the slots for the `Student` class, so basically our `Student` instances can only have a `name` attribute. We can add additional attributes by just specifying them in the slots for `Student`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Student(Person):\n",
    "    __slots__ = 'school', 'student_number'\n",
    "    \n",
    "    def __init__(self, name, school, student_number):\n",
    "        super().__init__(name)\n",
    "        self.school = school\n",
    "        self.student_number = student_number"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = Student('James', 'MI6 Prep', '007')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "('James', 'MI6 Prep', '007')"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "s.name, s.school, s.student_number"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Although Python does not currently disallow redefining slota in a subclass, it may in the future, and it can also cause unexpected behavior, so don't do it."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "When we subclass a slot-less class, and define slots for the subclass, then we get a similar behavior to oiur first example - the subclass has both an instance dictionary and slots:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    def __init__(self, name):\n",
    "        self.name = name\n",
    "        \n",
    "class Student(Person):\n",
    "    __slots__ = 'age', \n",
    "    \n",
    "    def __init__(self, name, age):\n",
    "        super().__init__(name)\n",
    "        self.age = age"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = Student('Python', 30)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "('Python', 30, {'name': 'Python'})"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "s.name, s.age, s.__dict__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see, the `age` attribute is stored in a slot, but the `name` attribute, defined in the slot-less `Person` class ends up in `Student`'s instance dictionary."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As we'll see later, behave essentially the same as properties - neither of them are actually stored in an instance dictionary - the additional effect of slots is that it (may) remove the need for an instance dictionary entirely."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So, when we define a property in a class, we don't need to specify it in the slots if we want to use slots:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    __slots__ = '_name', 'age'\n",
    "    \n",
    "    def __init__(self, name, age):\n",
    "        self.name = name\n",
    "        self.age = age\n",
    "        \n",
    "    @property\n",
    "    def name(self):\n",
    "        return self._name\n",
    "    \n",
    "    @name.setter\n",
    "    def name(self, name):\n",
    "        self._name = name"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = Person('Eric', 78)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So `p` has a property `name` and a (slotted) attribute `age`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "('Eric', 78)"
      ]
     },
     "execution_count": 21,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.name, p.age"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And we also do not have an instance dictionary:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "'Person' object has no attribute '__dict__'\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    print(p.__dict__)\n",
    "except AttributeError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So, as we can see neither the property `name` not the slotted attribute `age` are stored in an instance dictionary.\n",
    "\n",
    "In fact, they are very much related - to something called descriptors, which we'll study later.\n",
    "\n",
    "But let me just show you a quick preview of it."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Descriptors are objects that implement certain special functions (of course!) - just like iterators are objects that implement the special functions `__iter__` asnd `__next__`.\n",
    "\n",
    "For data descriptors, we implement the `__get__` and `__set__` methods (some others too, but those are enough for now)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So let's look at the attributes of the property `name` first:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(True, True)"
      ]
     },
     "execution_count": 23,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "hasattr(Person.name, '__get__'), hasattr(Person.name, '__set__')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And now let's see the slotted attribute `age`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(True, True)"
      ]
     },
     "execution_count": 24,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "hasattr(Person.age, '__get__'), hasattr(Person.age, '__set__')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aha! See, both implement these methods!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And by the way, remember when I said that the `property` class was just a convenience class? Well, in fact it basically creates an class for us that implements the `__get__`, `__set__`, etc methods based on the methods we specify for `fget`, `fset`, etc respectively."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Lastly, we have seen that we can have classes that have both a dictionary and slots - we got those when we used inheritance."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "But when we define without the `__slots__` attribute then it has an instance dictionary but no slots, and when we define `__slots__` it has slots but no instance dictionary."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can actually define classes that have both, simply by specifying `__dict__` as **one of the slots**:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    __slots__ = 'name', '__dict__'\n",
    "    \n",
    "    def __init__(self, name, age):\n",
    "        self.name = name\n",
    "        self.age = age"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = Person('Alex', 19)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "('Alex', 19, {'age': 19})"
      ]
     },
     "execution_count": 27,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.name, p.age, p.__dict__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As we can see, we have an instance dictionary (that contains `age` since it was not defined in the `__slots__`, and `name` which was defined as a slot however, is not fouind in the instance dictionary."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Of course, since we have an instance dictionary, we can add and remove arbitrary attributes at \"run-time\":"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [],
   "source": [
    "p.school = 'Berkeley'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'age': 19, 'school': 'Berkeley'}"
      ]
     },
     "execution_count": 29,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.__dict__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
